package IC.SEMANTIC;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import IC.BinaryOps;
import IC.AST.ArrayLocation;
import IC.AST.Assignment;
import IC.AST.Break;
import IC.AST.Call;
import IC.AST.CallStatement;
import IC.AST.Continue;
import IC.AST.Expression;
import IC.AST.ExpressionBlock;
import IC.AST.Field;
import IC.AST.Formal;
import IC.AST.ICClass;
import IC.AST.If;
import IC.AST.Length;
import IC.AST.LibraryMethod;
import IC.AST.Literal;
import IC.AST.LocalVariable;
import IC.AST.Location;
import IC.AST.LogicalBinaryOp;
import IC.AST.LogicalUnaryOp;
import IC.AST.MathBinaryOp;
import IC.AST.MathUnaryOp;
import IC.AST.Method;
import IC.AST.NewArray;
import IC.AST.NewClass;
import IC.AST.PrimitiveType;
import IC.AST.Program;
import IC.AST.Return;
import IC.AST.Statement;
import IC.AST.StatementsBlock;
import IC.AST.StaticCall;
import IC.AST.StaticMethod;
import IC.AST.This;
import IC.AST.UserType;
import IC.AST.VariableLocation;
import IC.AST.VirtualCall;
import IC.AST.VirtualMethod;
import IC.AST.Visitor;
import IC.AST.While;
import IC.Symbols.Kind;
import IC.Symbols.Symbol;
import IC.Symbols.SymbolTable;
import IC.Symbols.SymbolTableType;
import IC.Types.ArrayType;
import IC.Types.ClassType;
import IC.Types.MethodType;
import IC.Types.Type;
import IC.Types.TypeTable;

public class SemanticAnalysisVisitor implements Visitor {

    private TypeTable typeTable;
    private SymbolTable symbolTable;
    private SemanticError semanticError = null;
    private boolean mainMethodSeen = false;
    private int nestedWhileCount = 0;
    private Set<Literal> detectedOverflows;

    public SemanticAnalysisVisitor(TypeTable typeTable, String symbolTableName) {
        this.typeTable = typeTable;
        symbolTable = new SymbolTable(SymbolTableType.Global, symbolTableName);
        detectedOverflows = new HashSet<Literal>();
    }

    public SymbolTable getGlobalSymTable() {
        return symbolTable;
    }

    public Object visit(Program program) {
    	Object returnValue = null;
        for (ICClass icClass : program.getClasses()) {
            SymbolTable parentScope = icClass.parentClassExists() ? icClass.getParentSemanticType().getIcClass().getClassSymbolTable() : symbolTable;
            SymbolTable currentSymbolTable = new SymbolTable(SymbolTableType.Class, icClass.getName());
            currentSymbolTable.setParentSymbolTable(parentScope);
            currentSymbolTable.setAstNode(icClass);
            icClass.setSymbol(new Symbol(icClass.getName(), icClass.getSemanticType(), Kind.Class, program));
            if (!SafeInsertSymbol(icClass.getSymbolTableEntry(), symbolTable, icClass.getLine()))
            	return returnValue;
            icClass.setClassSymbolTable(currentSymbolTable);

            // add all methods and fields
            for (Method currentMethod : icClass.getMethods()) {
                if (currentSymbolTable.FindSymbol(currentMethod.getName(), null, false) != null) {
                	String errorMsg = String.format("Identifier '%s' already defined.",currentMethod.getName());
                    semanticError = new SemanticError(currentMethod.getLine(), errorMsg);
                    return returnValue;
                }
                
                Kind methodKind = (currentMethod instanceof VirtualMethod) ? Kind.VirtualMethod : Kind.StaticMethod;

                // define symbols in the appropriate place
                Symbol parentSymbol = currentSymbolTable.FindSymbol(currentMethod.getName(), null, true);
                if (parentSymbol != null && !(parentSymbol.getType() == (MethodType)currentMethod.getSemanticType() && parentSymbol.getKind() == methodKind)) {
                	String errorMsg = String.format("Identifier '%s' defined in parent class.",currentMethod.getName());
                    semanticError = new SemanticError(currentMethod.getLine(), errorMsg);
                    return returnValue;
                }

                Symbol methodSymbol = new Symbol(currentMethod.getName(), (MethodType)currentMethod.getSemanticType(), methodKind);
                methodSymbol.setAstNode(currentMethod);
				currentSymbolTable.InsertSymbol(methodSymbol);
                currentMethod.setSymbol(methodSymbol);
                boolean isMain = isMainMethod(currentMethod);
                if (isSemanticErrorEncountered()) 
                	return returnValue;
                if (isMain)
                	program.setMain(currentMethod);

            }

            for (Field icField : icClass.getFields()) {
                if (currentSymbolTable.FindSymbol(icField.getName(), null, true) != null) {
                	String errorMsg = String.format("Identifier '%s' already defined.",icField.getName());
                    semanticError = new SemanticError(icField.getLine(), errorMsg);
                    return returnValue;
                }

                icField.setSymbol(new Symbol(icField.getName(), icField.getType().getSemantictype(), Kind.Field));
				currentSymbolTable.InsertSymbol(icField.getSymbolTableEntry());
            }

            // setting convention for "this"
			currentSymbolTable.InsertSymbol(new Symbol("$this", icClass.getSemanticType(), Kind.This));

        }

        for (ICClass icClass : program.getClasses()) {
            icClass.accept(this);
            if (isSemanticErrorEncountered())
            	return returnValue;
        }

        if (!mainMethodSeen) {
            semanticError = new SemanticError(0, "main not found.");
            return returnValue;
        }
        
        if (detectedOverflows.size() != 0){
        	Literal icOverflowingLiteral = detectedOverflows.iterator().next();
        	String errorMsg = String.format("Overflow detected for literal '%s'.",icOverflowingLiteral.getValue());
            semanticError = new SemanticError(icOverflowingLiteral.getLine(), errorMsg);
            return returnValue;
        }
        
        
        returnValue = symbolTable;
        return returnValue;
    }

    public Object visit(ICClass icClass) {
    	Object returnValue = null;
    	
        for (Method icMethod : icClass.getMethods()) {
            icMethod.SetContainingSymbolTable(icClass.getClassSymbolTable());
            icMethod.accept(this);
            if (isSemanticErrorEncountered())
            	return returnValue;
        }

        for (Field icField : icClass.getFields()) {
            icField.SetContainingSymbolTable(icClass.getClassSymbolTable());
            icField.accept(this);
            if (isSemanticErrorEncountered())
            	return returnValue;
        }

        return icClass.getClassSymbolTable();
    }

    public Object GenericMethodVisit(Method icMethod, boolean staticMethodIndicator){
    	Symbol symbolOfReturn = null;
    	Object returnValue = null;
        SymbolTable methodSymbolTable = new SymbolTable(SymbolTableType.Method, icMethod.getName());
        if (staticMethodIndicator)
        	methodSymbolTable.setStaticScope();
        methodSymbolTable.setParentSymbolTable(icMethod.GetContainingSymbolTable());
        methodSymbolTable.setAstNode(icMethod);
        icMethod.SetContainingSymbolTable(methodSymbolTable);
        // setting return type convention
		symbolOfReturn = new Symbol("$ret", icMethod.getSemanticType(), Kind.ReturnVariable);
		
        if (!SafeInsertSymbol(symbolOfReturn, methodSymbolTable, icMethod.getLine()))
        	return returnValue;
        
        icMethod.setReturnSymbolTableSymbol(symbolOfReturn);

        for (Formal formal : icMethod.getFormals()) {
            formal.SetContainingSymbolTable(methodSymbolTable);
            formal.accept(this);
            if (isSemanticErrorEncountered()) 
            	return returnValue;
        }

        for (Statement statement : icMethod.getStatements()) {     	
            statement.SetContainingSymbolTable(methodSymbolTable);
            statement.accept(this);
            if (isSemanticErrorEncountered()) 
            	return returnValue;
        }

        returnValue = methodSymbolTable;
        
        return methodSymbolTable;
    }

    // safely add new symbol and return true if no error detected
	private boolean SafeInsertSymbol(Symbol symbolToInsert,
			SymbolTable methodSymbolTable, int lineNumber) {
		Symbol previousSymbol = methodSymbolTable.InsertSymbol(symbolToInsert);
        if (previousSymbol != null){
    		String errorMsg = String.format("The %s '%s' already defined.",symbolToInsert.getKind().name().toLowerCase(),previousSymbol.getName());
    		semanticError = new SemanticError(lineNumber, errorMsg);
        }
        return !isSemanticErrorEncountered();
	}

    public Object visit(Assignment icAssignment) {
        Location locationVariable = icAssignment.getVariable();
        locationVariable.SetContainingSymbolTable(icAssignment.GetContainingSymbolTable());
        locationVariable.accept(this);
        if (!isSemanticErrorEncountered()) {
        	Expression expressionAssignment = icAssignment.getAssignment();
        	expressionAssignment.SetContainingSymbolTable(icAssignment.GetContainingSymbolTable());
        	expressionAssignment.accept(this);
        	if (!isSemanticErrorEncountered() && !expressionAssignment.getSemanticType().isSub(locationVariable.getSemanticType())) {
        		String errorMsg = String.format("Can't assign '%s' into '%s' .",expressionAssignment.getSemanticType().getTypeName(),locationVariable.getSemanticType().getTypeName());
        		semanticError = new SemanticError(icAssignment.getLine(), errorMsg);
        	}
        }
        return null;
    }

    public Object visit(Return icReturn) {
        icReturn.setSymbol(icReturn.GetContainingSymbolTable().FindSymbol("$ret", null, true));
        Type returnType = ((MethodType)icReturn.getSymbolTableEntry().getType()).getMethodReturnType();
        
        if (icReturn.hasValue()) {
            Expression val = icReturn.getValue();
            val.SetContainingSymbolTable(icReturn.GetContainingSymbolTable());
            val.accept(this);
            if (semanticError == null && !val.getSemanticType().isSub(returnType)) {
            	String errorMsg = String.format("'%s' is returned instead of '%s' .",val.getSemanticType().getTypeName(),icReturn.getSymbolTableEntry().getType().getTypeName());
                semanticError = new SemanticError(icReturn.getLine(), errorMsg);
            }

        } else if (returnType != typeTable.getVoidPrimitiveType()) {
        	String errorMsg = String.format("Return type should be '%s' .",icReturn.getSymbolTableEntry().getType().getTypeName());
            semanticError = new SemanticError(icReturn.getLine(), errorMsg);
        }

        return null;
    }

    public Object visit(If icIf) {
        Statement ifOperation = icIf.getOperation();
        ifOperation.SetContainingSymbolTable(icIf.GetContainingSymbolTable());
        Expression ifBooleanExpression = icIf.getCondition();
        ifBooleanExpression.SetContainingSymbolTable(icIf.GetContainingSymbolTable());
        ifBooleanExpression.accept(this);
        if (!isSemanticErrorEncountered()) {
        	ifOperation.accept(this);
        	if (!isSemanticErrorEncountered()) {
        		if (icIf.hasElse()) {
        			Statement elseOp = icIf.getElseOperation();
        			elseOp.SetContainingSymbolTable(icIf.GetContainingSymbolTable());
        			elseOp.accept(this);
        		}
        		if (!isSemanticErrorEncountered() && ifBooleanExpression.getSemanticType() != typeTable.getBoolPrimitiveType())
        			semanticError = new SemanticError(ifBooleanExpression.getLine(), "If condition must be evaluated to boolean type.");
        	}
        }
        return null;
    }

    public Object visit(While icWhile) {
        Statement whileOperation = icWhile.getOperation();
        whileOperation.SetContainingSymbolTable(icWhile.GetContainingSymbolTable());

        Expression whileBooleanExpression = icWhile.getCondition();
        whileBooleanExpression.SetContainingSymbolTable(icWhile.GetContainingSymbolTable());
        whileBooleanExpression.accept(this);
        if (!isSemanticErrorEncountered()) {
        	nestedWhileCount++;
        	whileOperation.accept(this);
        	nestedWhileCount--;
        	if (semanticError == null && whileBooleanExpression.getSemanticType() != typeTable.getBoolPrimitiveType()) {
        		semanticError = new SemanticError(whileBooleanExpression.getLine(), "While condition must be evaluated to boolean type.");
        	}
        }
        return null;
    }

    public Object visit(StatementsBlock icStatementsBlock) {
        SymbolTable blockAreaSymbolTable = new SymbolTable(SymbolTableType.Block, "statement block in " + icStatementsBlock.GetContainingSymbolTable().getTableName());
        blockAreaSymbolTable.setParentSymbolTable(icStatementsBlock.GetContainingSymbolTable());
        blockAreaSymbolTable.setAstNode(icStatementsBlock);
        
        for (Statement icStatement : icStatementsBlock.getStatements()) {
            icStatement.SetContainingSymbolTable(blockAreaSymbolTable);
            icStatement.accept(this);
            if (isSemanticErrorEncountered()) 
            	break;
        }
        return null;
    }

    public Object visit(LocalVariable icLocalVariable) {
        // analyze container as needed
        if (icLocalVariable.GetContainingSymbolTable().FindSymbol(icLocalVariable.getName(), null, false) != null) {
        	String errorMsg = String.format("Variable '%s' already defined.",icLocalVariable.getName());
            semanticError = new SemanticError(icLocalVariable.getLine(), errorMsg);
            return null;
        }

        IC.Types.Type localVariableType = icLocalVariable.getType().getSemantictype();

        if (icLocalVariable.hasInitValue()) {
            Expression localVariableExpression = icLocalVariable.getInitValue();
            localVariableExpression.SetContainingSymbolTable(icLocalVariable.GetContainingSymbolTable());
            localVariableExpression.accept(this);
            if (isSemanticErrorEncountered())
            	return null;

            IC.Types.Type localVariableExpressionType = localVariableExpression.getSemanticType();
            if (!localVariableExpressionType.isSub(localVariableType)) {
            	String errorMsg = String.format("Can't assign '%s' into '%s'.",localVariableExpressionType.getTypeName(),localVariableType.getTypeName());
                semanticError = new SemanticError(icLocalVariable.getLine(), errorMsg);
                return null;
            }
        }

        Symbol localVariableSymbol = new Symbol(icLocalVariable.getName(), localVariableType, Kind.Variable);
        localVariableSymbol.setAstNode(icLocalVariable);
        icLocalVariable.setSymbol(localVariableSymbol);
        SafeInsertSymbol(localVariableSymbol, icLocalVariable.GetContainingSymbolTable(), icLocalVariable.getLine());
        return null;
    }

    public Object visit(VariableLocation icVariableLocation) {
    	// use the correct containing block
        SymbolTable containingBlock = icVariableLocation.isExternal() ? SymbolTableOfExpressionLocation(icVariableLocation.getLocation(), icVariableLocation.GetContainingSymbolTable()) : icVariableLocation.GetContainingSymbolTable();

        if (!isSemanticErrorEncountered()) {
        	Symbol variableLocationSymbol = containingBlock.FindSymbol(icVariableLocation.getName(), null, true);
        	if (variableLocationSymbol == null || !variableLocationSymbol.getKind().FindAsLocation()) {
        		String errorMsg = String.format("Unknown field or variable '%s'.",icVariableLocation.getName());
        		semanticError = new SemanticError(icVariableLocation.getLine(), errorMsg);
        	}
        	else{
        		icVariableLocation.setSymbol(variableLocationSymbol);
        		icVariableLocation.setSemanticType(variableLocationSymbol.getType());
        	}
        }
        return null;
    }

    public Object visit(ArrayLocation icArrayLocation) {
        Expression arrayExpression = icArrayLocation.getArray();
        arrayExpression.SetContainingSymbolTable(icArrayLocation.GetContainingSymbolTable());
        arrayExpression.accept(this);
        if (!isSemanticErrorEncountered()) {
        	Expression arrayIndexExpression = icArrayLocation.getIndex();
        	arrayIndexExpression.SetContainingSymbolTable(icArrayLocation.GetContainingSymbolTable());
        	arrayIndexExpression.accept(this);
        	if (!isSemanticErrorEncountered()) {
        		if (!(arrayExpression.getSemanticType() instanceof ArrayType)) {
        			semanticError = new SemanticError(icArrayLocation.getLine(), "'[' operator can be used only on array types.");
        		}
        		else if (arrayIndexExpression.getSemanticType() != this.typeTable.getIntegerPrimitiveType()) {
        			semanticError = new SemanticError(icArrayLocation.getLine(), "Index of Array must be integer.");
        		}
        		else {
        			ArrayType arrayType = (ArrayType)arrayExpression.getSemanticType();
        			icArrayLocation.setSemanticType(arrayType.getArrayType());
        		}
        	}
        }
        return null;
    }

    public Object visit(StaticCall icStaticCall) {
        SymbolTable containingSymbolTable = ((ClassType)icStaticCall.getSemanticType()).getIcClass().getClassSymbolTable();
        Symbol callSymbol = containingSymbolTable.FindSymbol(icStaticCall.getName(), true, false);
        if (callSymbol == null || callSymbol.getKind() != Kind.StaticMethod) {
        	String errorMsg = String.format("Unknown static method '%s'.",icStaticCall.getName());
        	semanticError = new SemanticError(icStaticCall.getLine(), errorMsg);
        }
        else{
        	icStaticCall.setSymbol(callSymbol);
        	GenericCallVisit(icStaticCall.GetContainingSymbolTable(), callSymbol, icStaticCall);
        }
        return null;
    }

    public Object visit(VirtualCall icVirtualCall) {
    	// use the correct containing block
        SymbolTable containingBlock = icVirtualCall.isExternal() ? SymbolTableOfExpressionLocation(icVirtualCall.getLocation(), icVirtualCall.GetContainingSymbolTable()) : icVirtualCall.GetContainingSymbolTable();
        if (isSemanticErrorEncountered()) 
        	return null;
        Boolean onlyStatic = icVirtualCall.isExternal() ? false : null;
        Symbol virtualCallSymbol = containingBlock.FindSymbol(icVirtualCall.getName(), onlyStatic , true);
        if (virtualCallSymbol == null || (virtualCallSymbol.getKind() != Kind.StaticMethod && virtualCallSymbol.getKind() != Kind.VirtualMethod)) {
        	String errorMsg = String.format("Unknown method '%s'.",icVirtualCall.getName());
        	semanticError = new SemanticError(icVirtualCall.getLine(), errorMsg);
        }
        else{
        	icVirtualCall.setSymbol(virtualCallSymbol);
        	GenericCallVisit(containingBlock, virtualCallSymbol, icVirtualCall);
        }
        return null;
    }

    public Object visit(This icThis) {
        Symbol thisSymbol = icThis.GetContainingSymbolTable().FindSymbol("$this", null, true);
        if (thisSymbol != null) {
        	icThis.setSymbol(thisSymbol);
        	icThis.setSemanticType(thisSymbol.getType());
        }
        else
        	semanticError = new SemanticError(icThis.getLine(), "'this' can't be used outside of virtual methods.");
        return null;
    }

    public Object visit(NewArray icNewArray) {
        Expression arraySizeExpression = icNewArray.getSize();
        arraySizeExpression.SetContainingSymbolTable(icNewArray.GetContainingSymbolTable());
        arraySizeExpression.accept(this);

        if (semanticError == null && arraySizeExpression.getSemanticType() != typeTable.getIntegerPrimitiveType())
            semanticError = new SemanticError(icNewArray.getLine(), "Size of array must be integer.");
        return null;
    }

    public Object visit(Length icLength) {
        Expression arrayExpression = icLength.getArray();
        arrayExpression.SetContainingSymbolTable(icLength.GetContainingSymbolTable());
        arrayExpression.accept(this);
        
        if (!isSemanticErrorEncountered()) 
        {
        	if (arrayExpression.getSemanticType() instanceof ArrayType)
        		icLength.setSemanticType(this.typeTable.getIntegerPrimitiveType());
        	else
        		semanticError = new SemanticError(icLength.getLine(), "'length' is used only for arrays.");
        }
        return null;
    }

    public Object visit(MathBinaryOp icMathBinaryOp) {
        Expression firstOperandExpression = icMathBinaryOp.getFirstOperand();
        firstOperandExpression.SetContainingSymbolTable(icMathBinaryOp.GetContainingSymbolTable());
        firstOperandExpression.accept(this);
        if (!isSemanticErrorEncountered()) {
        	Expression secondOperandExpression = icMathBinaryOp.getSecondOperand();
        	secondOperandExpression.SetContainingSymbolTable(icMathBinaryOp.GetContainingSymbolTable());
        	secondOperandExpression.accept(this);
        	if (!isSemanticErrorEncountered()) {
        		IC.Types.Type firstOperandType = firstOperandExpression.getSemanticType();
        		IC.Types.Type secondOperandType = secondOperandExpression.getSemanticType();

        		if (icMathBinaryOp.getOperator() == BinaryOps.PLUS &&
        				firstOperandType == typeTable.getStringPrimitiveType()
        				&& secondOperandType == typeTable.getStringPrimitiveType()) {
        			icMathBinaryOp.setSemanticType(typeTable.getStringPrimitiveType());
        		} else {
        			if (firstOperandType != typeTable.getIntegerPrimitiveType() ||
        					secondOperandType != typeTable.getIntegerPrimitiveType()) {
        				String errorMessage = String.format("operand for operator %s must be used with Integer (and not with %s).", icMathBinaryOp.getOperator().getDescription(),secondOperandExpression.getSemanticType().getTypeName());
        				semanticError = new SemanticError(icMathBinaryOp.getLine() , errorMessage);
        			}
        			icMathBinaryOp.setSemanticType(typeTable.getIntegerPrimitiveType());
        		}
        	}
        }
        return null;
    }

    public Object visit(LogicalBinaryOp icLogicalBinaryOp) {
        Expression firstOperandExpression = icLogicalBinaryOp.getFirstOperand();
        firstOperandExpression.SetContainingSymbolTable(icLogicalBinaryOp.GetContainingSymbolTable());
        firstOperandExpression.accept(this);
        if (!isSemanticErrorEncountered()) {
        	Expression secondOperandExpression = icLogicalBinaryOp.getSecondOperand();
        	secondOperandExpression.SetContainingSymbolTable(icLogicalBinaryOp.GetContainingSymbolTable());
        	secondOperandExpression.accept(this);
        	if (!isSemanticErrorEncountered()) {
        		IC.Types.Type firstOperandType = firstOperandExpression.getSemanticType();
        		IC.Types.Type secondOperandType = secondOperandExpression.getSemanticType();

        		BinaryOps binaryOperator = icLogicalBinaryOp.getOperator();
        		if (binaryOperator == BinaryOps.LAND || binaryOperator == BinaryOps.LOR) {
        			if (firstOperandType != typeTable.getBoolPrimitiveType() || secondOperandType != typeTable.getBoolPrimitiveType()) {
        				String errorMsg = String.format("operands must be boolean when using '%s' operator.",binaryOperator.getDescription());
        				semanticError = new SemanticError(icLogicalBinaryOp.getLine() , errorMsg);
        				return null;
        			}

        		} else if (binaryOperator == BinaryOps.GT || binaryOperator == BinaryOps.GTE ||
        				binaryOperator == BinaryOps.LT || binaryOperator == BinaryOps.LTE) {

        			if (firstOperandType != typeTable.getIntegerPrimitiveType() || secondOperandType != typeTable.getIntegerPrimitiveType()) {
        				String errorMsg = String.format("operands must be Integer when using '%s' operator.",binaryOperator.getDescription());
        				semanticError = new SemanticError(icLogicalBinaryOp.getLine() , errorMsg);
        				return null;
        			}
        		} else if (binaryOperator == BinaryOps.EQUAL || binaryOperator == BinaryOps.NEQUAL) {
        			if (!firstOperandType.isSub(secondOperandType) && !secondOperandType.isSub(firstOperandType)) {
        				String errorMsg = String.format("operands must have relation of inheritance when using '%s' operator.",binaryOperator.getDescription());
        				semanticError = new SemanticError(icLogicalBinaryOp.getLine() , errorMsg);
        				return null;
        			}
        		}
        		icLogicalBinaryOp.setSemanticType(this.typeTable.getBoolPrimitiveType());
        	}
        }
        return null;
    }

    public Object visit(MathUnaryOp icMathUnaryOp) {
        Expression operandExpression =  icMathUnaryOp.getOperand();
        operandExpression.SetContainingSymbolTable(icMathUnaryOp.GetContainingSymbolTable());
        operandExpression.accept(this);
        
        if (!isSemanticErrorEncountered()) {
        	if (operandExpression.getSemanticType() != typeTable.getIntegerPrimitiveType()) {
        		String errorMsg = String.format("operands must be Integer when using '%s' operator.",icMathUnaryOp.getOperator().getDescription());
        		semanticError = new SemanticError(icMathUnaryOp.getLine() , errorMsg);
        	}
        	else{
        		icMathUnaryOp.setSemanticType(typeTable.getIntegerPrimitiveType());
        	}
        }
        
        // special case for Integer.MIN_VALUE
        detectedOverflows.remove(operandExpression);
        
        return null;
    }

    public Object visit(LogicalUnaryOp icLogicalUnaryOp) {
        Expression operandExpression =  icLogicalUnaryOp.getOperand();
        operandExpression.SetContainingSymbolTable(icLogicalUnaryOp.GetContainingSymbolTable());
        operandExpression.accept(this);

        if (!isSemanticErrorEncountered()) {

        	if (operandExpression.getSemanticType() != typeTable.getBoolPrimitiveType()) {
        		String errorMsg = String.format("operands must be Boolean when using '%s' operator.",icLogicalUnaryOp.getOperator().getDescription());
        		semanticError = new SemanticError(icLogicalUnaryOp.getLine() , errorMsg);
        	}
        	else
        		icLogicalUnaryOp.setSemanticType(this.typeTable.getBoolPrimitiveType());
        }
        return null;
    }

    public Object visit(Literal icLiteral) {
    	switch (icLiteral.getType()) {
    	case NULL:
    		icLiteral.setSemanticType(typeTable.getNullPrimitiveType());
    		break;
    	case STRING:
    		icLiteral.setSemanticType(typeTable.getStringPrimitiveType());
    		break;
    	case INTEGER:
    		icLiteral.setSemanticType(typeTable.getIntegerPrimitiveType());
    		if ((Long)icLiteral.getValue() > Integer.MAX_VALUE)
    			detectedOverflows.add(icLiteral);
    		break;
    	case TRUE:
    		icLiteral.setSemanticType(typeTable.getBoolPrimitiveType());
    		break;
    	case FALSE:
    		icLiteral.setSemanticType(typeTable.getBoolPrimitiveType());
    		break;
    	}
        return null;
    }

    public Object visit(ExpressionBlock icExpressionBlock) {
        Expression blockExpression = icExpressionBlock.getExpression();
        blockExpression.SetContainingSymbolTable(icExpressionBlock.GetContainingSymbolTable());
        blockExpression.accept(this);
        
        if (!isSemanticErrorEncountered())
        	icExpressionBlock.setSemanticType(blockExpression.getSemanticType());
        
        return null;
    }

    private boolean isMainMethod(Method method) {
        MethodType methodType = (MethodType) method.getSemanticType();

        boolean isMain = (method.getName().equals("main") &&
                methodType.getMethodParametersTypeArray().length == 1 &&
                methodType.getMethodParametersTypeArray()[0] == this.typeTable.getArrayType(this.typeTable.getStringPrimitiveType()) &&
                methodType.getMethodReturnType() == this.typeTable.getVoidPrimitiveType() &&
                method instanceof StaticMethod);
        if (mainMethodSeen && isMain) {
            semanticError = new SemanticError(method.getLine(), "'main' is defined in more than one place.");
        }

        if (isMain)
            mainMethodSeen = isMain;
        
        return isMain;
    }
    
    private void GenericCallVisit(SymbolTable containingSymbolTable, Symbol methodSymbol, Call genericCall) {
    	List<Expression> callArguments = genericCall.getArguments();
    	IC.Types.Type[] parametersTypeArray = ((MethodType)methodSymbol.getType()).getMethodParametersTypeArray();
    	if (parametersTypeArray.length != callArguments.size()) {
    		String errorMsg = String.format("wrong number of arguments in calling of method '%s'.",genericCall.getName());
    		semanticError = new SemanticError(genericCall.getLine(), errorMsg);
    	}
    	else{
    		innerCallVisitorHandler(methodSymbol, genericCall, callArguments,
					parametersTypeArray);
    	}
    }

    public Object visit(VirtualMethod method) {
		return GenericMethodVisit(method, false);
    }

    public Object visit(StaticMethod method) {
			return GenericMethodVisit(method, true);
    }

    public Object visit(LibraryMethod method) {
			return GenericMethodVisit(method, true);
    }

    public Object visit(Formal formal) {
    	Symbol symbol = new Symbol(formal.getName(), formal.getType().getSemantictype(), Kind.Parameter);
    	symbol.setAstNode(formal);
        if (SafeInsertSymbol(symbol, formal.GetContainingSymbolTable(), formal.getLine()))
        	formal.setSymbol(symbol);
        return null;
    }

    public Object visit(Break icBreak) {
        if (nestedWhileCount == 0)
            semanticError = new SemanticError(icBreak.getLine(), "Break statement not allowed outside of loop.");
        return null;
    }

    public Object visit(Continue icContinue) {
        if (this.nestedWhileCount == 0)
            semanticError = new SemanticError(icContinue.getLine(), "Continue statement not allowed outide of loop.");
        return null;
    }
    
    public Object visit(CallStatement icCallStatement) {
        icCallStatement.getCall().SetContainingSymbolTable(icCallStatement.GetContainingSymbolTable());
        icCallStatement.getCall().accept(this);
        return null;
    }
    
    public Object visit(Field field) {
        return null;
    }
    
    public Object visit(PrimitiveType type) {
        return null;  
    }

    public Object visit(UserType type) {
        return null;  
    }
    
    public Object visit(NewClass newClass) {
        return null;
    }
    
    public SemanticError getError() {
        return semanticError;
    }
    
    private boolean isSemanticErrorEncountered(){
    	return (semanticError != null);
    }
    
    private SymbolTable SymbolTableOfExpressionLocation(Expression expressionLocation, SymbolTable symbolTable) {
    	SymbolTable returnValue = null;
    	expressionLocation.SetContainingSymbolTable(symbolTable);
    	expressionLocation.accept(this);
    	if (!isSemanticErrorEncountered()){
    		returnValue = GetReturnValue(expressionLocation, returnValue);
    	}
    	return returnValue;
    }

	private SymbolTable GetReturnValue(Expression expressionLocation,
			SymbolTable returnValue) {
		IC.Types.Type expressionType = expressionLocation.getSemanticType();
		if (expressionType instanceof ClassType) {
			returnValue = ((ClassType)expressionType).getIcClass().getClassSymbolTable();
		}
		else{
			String errorMsg = String.format("Can't dereference the non-class type '%s'.",expressionType.getTypeName());
			semanticError = new SemanticError(expressionLocation.getLine(), errorMsg);
		}
		return returnValue;
	}
    


	private void innerCallVisitorHandler(Symbol methodSymbol, Call genericCall,
			List<Expression> callArguments, IC.Types.Type[] parametersTypeArray) {
		for (int i = 0; i < parametersTypeArray.length; i++) {
			Expression currentParameterExpression = callArguments.get(i);
			currentParameterExpression.SetContainingSymbolTable(genericCall.GetContainingSymbolTable());
			currentParameterExpression.accept(this);
			if (isSemanticErrorEncountered()) 
				break;
			else if (!currentParameterExpression.getSemanticType().isSub(parametersTypeArray[i])) {
				String errorMsg = String.format("Parameter number %d has an incorrect type.",++i);
				semanticError = new SemanticError(genericCall.getLine() ,errorMsg);
				break;
			}
		}
		if (!isSemanticErrorEncountered()) 
			genericCall.setSemanticType(((MethodType)methodSymbol.getType()).getMethodReturnType());
	}
}
